package com.compunit.composite.core;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.naming.OperationNotSupportedException;

import org.compunit.Provide;
import org.compunit.Require;

import com.compunit.composite.core.cx.CxExporter;
import com.compunit.composite.core.cx.CxImporter;
import com.compunit.composite.core.interfaces.IClassExporter;
import com.compunit.composite.core.interfaces.IComposer;
import com.compunit.composite.core.interfaces.cx.ICxBinding;
import com.compunit.composite.core.interfaces.cx.ICxExporter;
import com.compunit.composite.core.interfaces.cx.ICxMapping;
import com.compunit.composite.core.interfaces.cx.ICxUnit;

/**
 * <code>Composer</code> defines the implementation of
 * the foundation level utility unit that is responsible
 * for composing multiple components together into a
 * single component unit.
 *
 * @author Yi Wang (Neakor)
 * @version Creation date: 11-21-2009 22:33 EST
 * @version Modified date: 12-05-2009 22:05 EST
 */
public class Composer implements IComposer {
	/**
	 * The <code>ICxExporter</code> instance.
	 */
	private final ICxExporter cxexporter;
	/**
	 * The <code>IClassExporter</code> instance.
	 */
	private final IClassExporter classexporter;
	/**
	 * The <code>Map</code> of <code>String</code>
	 * component implementation class name to the
	 * <code>Class</code> object.
	 */
	private final Map<String, Class<?>> compclasses;
	
	/**
	 * Constructor of <code>Composer</code>.
	 */
	public Composer() {
		this.cxexporter = new CxExporter();
		this.classexporter = new ClassExporter();
		this.compclasses = new HashMap<String, Class<?>>();
	}

	@Override
	public void compose(final URL directory) throws URISyntaxException, IOException, OperationNotSupportedException {
		final String name = this.cxexporter.getName();
		this.cxexporter.export(directory, name+".cx");
		this.classexporter.export(directory, name+".jar");
	}

	@Override
	public void export(final URL dir, final String filename) throws URISyntaxException,	IOException, OperationNotSupportedException {
		throw new OperationNotSupportedException("Use compose method to export.");
	}

	@Override
	public void export(final String dir, final String filename) throws URISyntaxException, IOException, OperationNotSupportedException {
		throw new OperationNotSupportedException("Use compose method to export.");
	}

	@Override
	public void addComponent(final URL directory, final String name) throws ClassNotFoundException, IOException, URISyntaxException {
		// Initialize loaders.
		final URL classurl = this.getURL(directory, name+".jar");
		final URL cxurl = this.getURL(directory, name+".cx");
		final CxImporter cximporter = new CxImporter();
		cximporter.process(cxurl);
		final URLClassLoader loader = new URLClassLoader(new URL[] {classurl});
		// Load in component classes.
		final Iterable<ICxUnit> units = cximporter.getActivation();
		for(final ICxUnit unit : units) {
			final Class<?> c = loader.loadClass(unit.getClassname());
			this.compclasses.put(c.getName(), c);
			// Add to class exporter.
			this.classexporter.addClass(c);
		}
		// Carry over all other classes.
		final JarFile jar = new JarFile(new File(classurl.toURI()));
		final Enumeration<JarEntry> entries = jar.entries();
		while(entries.hasMoreElements()) {
			final JarEntry entry = entries.nextElement();
			final String full = entry.toString().replace("/", ".");
			final String classname = full.substring(0, full.lastIndexOf("."));
			final Class<?> c = loader.loadClass(classname);
			this.classexporter.addClass(c);
		}
	}
	
	/**
	 * Retrieve the URL using given directory and file name.
	 * @param directory The <code>URL</code> to the directory
	 * where the file resides.
	 * @param filename The <code>String</code> file name
	 * including the extension.
	 * @return The <code>URL</code> to the file.
	 * @throws IOException If file handling failed.
	 */
	private final URL getURL(final URL directory, final String filename) throws IOException {
		String raw = directory.getPath();
		if(!raw.endsWith("/")) raw += "/";
		final String path = raw.substring(raw.indexOf("/"), raw.length()) + filename;
		final File file = new File(path);
		if(!file.exists()) throw new IOException("File does not exist: ".concat(file.toString()));
		return file.toURI().toURL();
	}

	@Override
	public void addBinding(final ICxBinding binding) {
		// Check if already added.
		if(this.cxexporter.containsBinding(binding)) return;
		// Check if both consumer and provider classes are added.
		final Class<?> provider = this.compclasses.get(binding.getProvider());
		final Class<?> consumer = this.compclasses.get(binding.getConsumer());
		if(provider == null) throw new IllegalArgumentException("Binding class not yet added: " + binding.getProvider());
		else if(consumer == null) throw new IllegalArgumentException("Binding class not yet added: " + binding.getConsumer());
		// Check with class annotations.
		final Provide annop = provider.getAnnotation(Provide.class);
		final Require annor = consumer.getAnnotation(Require.class);
		if(annop == null || annor == null) throw new IllegalArgumentException("Binding does not match with class annotation: " + binding);
		final Class<?>[] provided = annop.value();
		final Class<?>[] required = annor.value();
		if(provided == null || required == null) throw new IllegalArgumentException("Binding does not match with class annotation: " + binding);
		for(final Class<?> p : provided) {
			for(final Class<?> r : required) {
				// Found a match, so valid.
				if(p.getName().equals(r.getName())) {
					this.cxexporter.addBinding(binding);
					return;
				}
			}
		}
		// No match found.
		throw new IllegalArgumentException("Binding does not match with class annotation: " + binding);
	}

	@Override
	public void addMapping(final ICxMapping mapping) {
		// Check if already added.
		if(this.cxexporter.containsMapping(mapping)) return;
		// Validate mapping.
		int valid = 0;
		int count = 0;
		final String interfacename = mapping.getInterface();
		final Iterable<String> classnames = mapping.getClasses();
		for(final String classname : classnames) {
			final Class<?> compclass = this.compclasses.get(classname);
			if(compclass != null) {
				if(mapping.isProvided()) {
					final Provide annop = compclass.getAnnotation(Provide.class);
					final Class<?>[] provided = annop.value();
					for(final Class<?> p : provided) {
						// Match found, so this class is valid.
						if(p.getName().equals(interfacename)) {
							valid++;
							break;
						}
					}
				} else {
					final Require annop = compclass.getAnnotation(Require.class);
					final Class<?>[] required = annop.value();
					for(final Class<?> r : required) {
						// Match found, so this class is valid.
						if(r.getName().equals(interfacename)) {
							valid++;
							break;
						}
					}
				}
			} else {
				throw new IllegalArgumentException("Component class has not been added: " + classname);
			}
			count++;
		}
		// Compare valid class count with total class count.
		if(valid != count) throw new IllegalArgumentException("Mapping contains invalid classes.");
		// Finally all valid, add to export cache.
		this.cxexporter.addMapping(mapping);
	}

	@Override
	public void removeBinding(final ICxBinding binding) {
		this.cxexporter.removeBinding(binding);
	}

	@Override
	public void removeMapping(final ICxMapping mapping) {
		this.cxexporter.removeMapping(mapping);
	}

	@Override
	public boolean containsBinding(final ICxBinding binding) {
		return this.cxexporter.containsBinding(binding);
	}

	@Override
	public boolean containsMapping(final ICxMapping mapping) {
		return this.cxexporter.containsMapping(mapping);
	}

	@Override
	public void setActivation(final ICxUnit[] classes) {
		// Check if all components are added.
		final int length = classes.length;
		for(int i = 0; i < length; i++) {
			final ICxUnit unit = classes[i];
			if(!this.compclasses.containsKey(unit.getClassname())) {
				throw new IllegalArgumentException("Component not yet added or is not a component: " + unit.getClassname());
			}
		}
		// Add to cx exporter.
		this.cxexporter.setActivation(classes);
	}

	@Override
	public void setName(final String name) {
		this.cxexporter.setName(name);
	}

	@Override
	public String getName() {
		return this.cxexporter.getName();
	}

	@Override
	public void cleanup() {
		this.cxexporter.cleanup();
		this.classexporter.cleanup();
		this.compclasses.clear();
	}

	@Override
	public boolean isEmpty() {
		return (this.cxexporter.isEmpty() && this.classexporter.isEmpty() && this.compclasses.isEmpty());
	}
}
